﻿using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq.Expressions;
using System.Data;
using System.Reflection;
using System.Collections;
using System.IO;
using System.Collections.ObjectModel;

using LeanEngine.Data.Linq.Mapping;
using LeanEngine.Data.Linq.Translator;
using LeanEngine.Entity;

namespace LeanEngine.Data.Linq
{
    internal sealed class QueryProvider : IDatabaseContext
    {
        private bool _isDisposed;
        private readonly DatabaseContext _dataContext;

        public QueryProvider(DatabaseContext dataContext)
        {
            _dataContext = dataContext;
        }

        #region IDatabaseProvider Members

        public TextWriter Log { get; set; }

        public object Execute(string query, MetaTable metaTable)
        {
            CheckDispose();

            if (string.IsNullOrEmpty(query))
                throw new ArgumentNullException("query");

            QueryInfo info = new QueryInfo(ResultShape.Sequence, query)
            {
                SourceMetadata = metaTable
            };

            // execute
            return Execute(info);
        }

        /// <summary>
        /// This is very the query is executed and the results are mapped to objects
        /// </summary>
        /// <param name="query"></param>
        /// <returns></returns>
        public object Execute(Expression query)
        {
            CheckDispose();

            QueryTranslator translator = new QueryTranslator();
            QueryInfo info = translator.Interpret(new QueryContext() { LinqExpression = query });

            return Execute(info);
        }

        private object Execute(QueryInfo info)
        {
            // build a SQL Command
            using (var command = new DataCommand())
            {
                command.CommandText = info.QueryText;

                // attempt to excute the query
                if (info.ResultShape == ResultShape.None)
                {
                    command.ExecuteNonQuery();
                }
                else if (info.ResultShape == ResultShape.SingleValue)
                {
                    return command.ExecuteScalar();
                }
                else
                {
                    var reader = command.ExecuteReader(CommandBehavior.SingleResult);

                    // log the command
                    if (Log != null) Log.WriteLine(command.CommandText);

                    // what is the CLR type of the returned rows?
                    Type resultEntityType =
                      (info.LambdaExpression == null) ? info.SourceMetadata.EntityType : info.LambdaExpression.Body.Type;

                    // Build a pipeline so that we can read and map the results from the database
                    // do this by dynamically creating an instance of ResultMapper<>
                    // the followig statement is equivalent to:
                    //    IEnumerable mappedResults = new ResultMapper<T>(info, reader, _dataContext.ChangeTracker);
                    IEnumerable mappedResults =
                      (IEnumerable)Activator.CreateInstance(typeof(ResultMapper<>)
                        .MakeGenericType(new[] { resultEntityType }),
                        BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance,
                        null, new object[] { info, reader }, null);

                    // are we expecting a single entity or a sequence of entities?
                    if (info.ResultShape == ResultShape.Sequence)
                    {
                        // Now read the results by enumerating through all mappedResults.
                        // This is not perfect but as I am sharing the database 
                        // connection between all queries and even write operations,
                        // I need to load all entities into memory by fully reading 
                        // them using the DbDataReader (mappedResults) – that is exactly what the 
                        // constructor of List<> does for me here.
                        // the followig line is equivalent to:
                        //    return new List<T>(mappedResults);
                        return Activator.CreateInstance(typeof(List<>).MakeGenericType(
                        new[] { resultEntityType }), new object[] { mappedResults });
                    }
                    else if (info.ResultShape == ResultShape.Singleton)
                    {
                        IEnumerator enumerator = mappedResults.GetEnumerator();
                        if (enumerator.MoveNext())
                        {
                            return enumerator.Current;
                        }
                        // if First or Single then throw an excpetion because no result was found
                        // you should always expect at least one element when First or Single are used
                        else if (!info.UseDefault)
                        {
                            throw new InvalidOperationException("No entity was found meeting the specified criteria.");
                        }
                    }
                }
                return null;
            }
        }

        public string GetQueryText(Expression query)
        {
            CheckDispose();

            QueryTranslator translator = new QueryTranslator();
            QueryInfo info = translator.Interpret(new QueryContext() { LinqExpression = query });

            return info.QueryText;
        }

        private void CheckDispose()
        {
            if (_isDisposed)
                throw new ObjectDisposedException(typeof(QueryProvider).Name);
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            _isDisposed = disposing;
        }

        #endregion
    }
}
